feat(packaging): add Debian (.deb) build via nfpm with systemd unit (v2)#7583
feat(packaging): add Debian (.deb) build via nfpm with systemd unit (v2)#7583JohnMcLear merged 15 commits intoether:developfrom
Conversation
First-class Debian packaging for Etherpad, producing etherpad_<version>_<arch>.deb artefacts for amd64 and arm64 from a single nfpm manifest. Installing the package gives users: - /opt/etherpad with a prebuilt, self-contained node_modules/ — no pnpm required at runtime, just `nodejs (>= 20)`. - etherpad system user/group, created via `adduser` in preinst. - /etc/etherpad/settings.json seeded from the template on first install, preserved across upgrades, removed on `purge`. Seed rewrites dbType from the template's dev-only `dirty` default to `sqlite`, pointed at /var/lib/etherpad/etherpad.db so fresh installs get an ACID-safe DB without manual config. sqlite is shipped by ueberdb2 (rusty-store-kv), so no additional apt deps are needed. - /var/lib/etherpad owned by etherpad:etherpad, writable under the hardened unit's ProtectSystem=strict. - /lib/systemd/system/etherpad.service — hardened unit (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies) with Restart=on-failure. - /usr/bin/etherpad CLI wrapper running `node --import tsx/esm`. CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both arches via native runners (ubuntu-latest + ubuntu-24.04-arm), smoke-tests the amd64 package end-to-end (install → verify sqlite default → systemctl start → curl /health → purge → confirm user removed), and attaches the artefacts to the GitHub Release. Re-introduces the work from ether#7559 (reverted in ether#7582) with two corrections: 1. Package name and all installed paths use `etherpad`, not `etherpad-lite` — matches the repo rename. Kept replaces/conflicts on `etherpad-lite` so any dev builds of the reverted PR upgrade cleanly. 2. Default dbType is `sqlite`, not `dirty`. The template's own comment says dirty is for testing only; shipping it by default to everyone who runs `apt install etherpad` is the wrong tradeoff for a production package. Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted reprepro) is intentionally out of scope — needs a governance decision on who holds the signing key. Recipes are documented in packaging/README.md. Refs ether#7529, ether#7559, ether#7582 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
Code Review by Qodo
1. Settings.json clobbered
|
Review Summary by QodoAdd Debian (.deb) packaging with systemd service and nfpm manifest
WalkthroughsDescription• Add native Debian (.deb) packaging via nfpm for amd64 and arm64 • Configure systemd service with hardened security settings and auto-restart • Implement pre/post install/remove scripts for user/group and config management • Default database to sqlite instead of dev-only dirty mode for production safety • Add CI workflow to build, smoke-test, and attach packages to GitHub releases Diagramflowchart LR
A["Source code<br/>+ node_modules"] -->|Stage| B["staging/opt/etherpad"]
B -->|nfpm manifest| C["nfpm.yaml"]
C -->|Build| D["etherpad_VERSION_ARCH.deb"]
E["preinstall.sh"] -->|Create user| F["etherpad system user"]
G["postinstall.sh"] -->|Configure| H["Settings + Symlinks"]
I["etherpad.service"] -->|Hardened unit| J["systemd service"]
K["CI workflow"] -->|Tag trigger| L["Build + Test + Release"]
File Changes1. packaging/scripts/preinstall.sh
|
Code Review by Qodo
1. Readonly /opt breaks startup
|
| # --- Sandboxing --------------------------------------------------------- | ||
| NoNewPrivileges=true | ||
| ProtectSystem=strict | ||
| ProtectHome=true | ||
| PrivateTmp=true | ||
| PrivateDevices=true | ||
| ProtectKernelTunables=true | ||
| ProtectKernelModules=true | ||
| ProtectKernelLogs=true | ||
| ProtectControlGroups=true | ||
| ProtectHostname=true | ||
| ProtectClock=true | ||
| RestrictRealtime=true | ||
| RestrictSUIDSGID=true | ||
| RestrictNamespaces=true | ||
| LockPersonality=true | ||
| MemoryDenyWriteExecute=false # Node's JIT needs W+X mappings | ||
| SystemCallArchitectures=native | ||
| RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK | ||
| UMask=0027 | ||
|
|
||
| ReadWritePaths=/var/lib/etherpad /var/log/etherpad /etc/etherpad | ||
|
|
There was a problem hiding this comment.
1. Readonly /opt breaks startup 🐞 Bug ☼ Reliability
The unit sets ProtectSystem=strict but Etherpad writes runtime files under settings.root/var during startup (for example var/js and var/installed_plugins.json). Because /opt/etherpad/var is neither created/redirected in postinstall nor included in ReadWritePaths, a fresh install can fail to start with mkdir/write errors under /opt/etherpad/var.
Agent Prompt
### Issue description
The systemd unit makes `/opt/etherpad` effectively read-only (`ProtectSystem=strict` + `ReadWritePaths` omits `/opt/etherpad/...`), but Etherpad writes to `path.join(settings.root, 'var', ...)` during startup, where `settings.root` resolves to `/opt/etherpad` in this package layout. This can prevent `etherpad.service` from starting.
### Issue Context
Etherpad startup awaits `checkForMigration()` which can write `${root}/var/installed_plugins.json`, and the express hooks create `${root}/var/js`. The package currently does not create or redirect `/opt/etherpad/var` to a writable location.
### Fix Focus Areas
- packaging/scripts/postinstall.sh[34-40]
- packaging/systemd/etherpad.service[22-44]
### Suggested fix
Implement one of the following (prefer the symlink approach to keep writes out of `/opt`):
1) **Symlink `/opt/etherpad/var` to a writable location**:
- In `postinstall.sh` (configure):
- `mkdir -p /var/lib/etherpad/var`
- `ln -sfn /var/lib/etherpad/var /opt/etherpad/var`
- `chown -R etherpad:etherpad /var/lib/etherpad/var`
- This keeps `ReadWritePaths=/var/lib/etherpad` sufficient.
2) **Allow `/opt/etherpad/var` writes explicitly**:
- Create `/opt/etherpad/var` in `postinstall.sh` and `chown` it to `etherpad:etherpad`.
- Add `/opt/etherpad/var` to `ReadWritePaths` in the unit.
Either way, ensure the directory exists before service start so `fs.mkdirSync(.../var/js)` and plugin migration writes don’t throw.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| - name: Setup Node | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: '22' |
|
|
||
| ## Building locally | ||
|
|
||
| Prereqs: Node 22, pnpm 10+, nfpm. |
There was a problem hiding this comment.
Node 24 makes more sense I think
There was a problem hiding this comment.
I follow Etherpad's own engine.node - if we want to bump node version we can, but this isn't the place to begin that bump IMHO.
| ## Installing | ||
|
|
||
| ```sh | ||
| sudo apt install ./dist/etherpad_2.6.1_amd64.deb |
There was a problem hiding this comment.
Do we want to make this more generic? This is already out of date.
…de LTS Addresses Qodo and SamTV12345 review feedback on ether#7583: - postinstall: symlink /opt/etherpad/var → /var/lib/etherpad/var so ProtectSystem=strict doesn't block runtime writes (var/js, installed_plugins.json, etc.). Existing ReadWritePaths covers it. - postinstall: seed installed_plugins.json with ep_etherpad-lite so checkForMigration() does not spawn `pnpm ls` on first boot — pnpm is not a runtime dep, and the bundled node_modules already contains every shipped plugin. Prevents network plugin installs at first run. - postremove: clean up the new var symlink on remove. - workflow: verify nfpm .deb sha256 against upstream checksums.txt before sudo dpkg -i (defense in depth). - workflow: bump Node 22 → 24 (current LTS, per SamTV12345). The deb Depends stays at nodejs (>= 20) to match Etherpad's engines.node. - workflow: smoke-test now asserts the var symlink and seeded installed_plugins.json exist post-install. - workflow: publish stable etherpad-latest_{amd64,arm64}.deb aliases alongside the versioned files in the GitHub Release. - README: bump Node guidance to 24, document /releases/latest URL, link to engines.node floor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit 725e73d |
Addresses second-round Qodo review on ether#7583: - bin/etherpad: switch from `--import tsx/.../esm` to `--require tsx/cjs`. server.ts uses `exports.start = ...` which throws under the ESM loader; the prod script in src/package.json uses tsx/cjs for the same reason. - postinstall: symlink /opt/etherpad/src/plugin_packages → /var/lib/etherpad/plugin_packages and chgrp /opt/etherpad/src/node_modules to etherpad with mode 2775. Otherwise admin-UI plugin install EACCESes — those are the dirs LinkInstaller writes to. - systemd unit: add /opt/etherpad/src/node_modules to ReadWritePaths so symlink creation by the etherpad user is allowed under ProtectSystem=strict. plugin_packages is already covered via the symlink into /var/lib/etherpad. - postremove: clean up the new plugin_packages symlink on remove. - workflow: tag filters were `v[0-9]+.[0-9]+.[0-9]+`, but Actions tag filters are globs, not regex. `[0-9]+` matches one character, so multi-digit tags like v2.10.0 would never trigger. Switch to `v*.*.*` / `v*.*.*-*`, matching handleRelease.yml. - workflow smoke test now asserts plugin_packages symlink target, ownership of plugin_packages and node_modules. - test-local.sh: new script that builds the .deb and runs the same smoke test in a throwaway systemd-enabled Docker container, so failures are caught before pushing. - README: document test-local.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit d1f51f3 |
| case "$1" in | ||
| install|upgrade) | ||
| if ! getent group etherpad >/dev/null 2>&1; then | ||
| addgroup --system etherpad | ||
| fi | ||
| if ! getent passwd etherpad >/dev/null 2>&1; then | ||
| adduser --system --ingroup etherpad \ | ||
| --home /var/lib/etherpad \ | ||
| --no-create-home \ | ||
| --shell /usr/sbin/nologin \ | ||
| --gecos "Etherpad service user" \ | ||
| etherpad | ||
| fi |
There was a problem hiding this comment.
1. Preinst depends not guaranteed 🐞 Bug ☼ Reliability
The preinstall maintainer script requires addgroup/adduser, but the package only declares adduser as a regular dependency, so dpkg -i on a minimal system can fail before unpacking if adduser isn’t already installed.
Agent Prompt
### Issue description
`preinstall.sh` calls `addgroup`/`adduser` during the `preinst` phase, but `adduser` is only in `Depends`. Direct installs via `dpkg -i` can run `preinst` before `adduser` is present, causing installation to abort.
### Issue Context
This is especially problematic because a `preinst` failure happens *before* unpacking, so follow-up dependency repair (`apt-get -f install`) may not be able to recover.
### Fix Focus Areas
Implement one of:
- **Debian-correct dependency**: add a Debian `Pre-Depends: adduser` (if nfpm supports it) so `adduser` is guaranteed before `preinst`.
- **Move user/group creation later**: shift user/group creation to `postinstall` (`configure`), and adjust packaging so unpack-time ownership does not require the `etherpad` group to already exist (e.g., set `/etc/default/etherpad` group to root in the payload and `chgrp` it in `postinstall`).
- packaging/scripts/preinstall.sh[1-18]
- packaging/nfpm.yaml[22-26]
- packaging/scripts/postinstall.sh[16-34]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
- systemd-in-docker on cgroups v2 needs --cgroupns=host and a writable /sys/fs/cgroup mount; the previous :ro version booted to nothing. - New --no-systemd mode: drops the systemd container in favour of plain ubuntu:24.04 + manual launch under the etherpad user. Validates the postinstall, wrapper, plugin paths, and /health without depending on the host's systemd-in-docker setup. Use it when --privileged systemd containers don't boot on your kernel/docker combo. - On systemd container exit the script now dumps the last 50 log lines and points at --no-systemd as the fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If ubuntu:24.04 isn't on disk and the registry is unreachable, fall back to whichever ubuntu/debian image is already cached (e.g. the jrei/systemd-ubuntu image we pulled for the systemd path). Avoids a registry round-trip on flaky networks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ine-safe test
src/node/utils/run_cmd.ts:
Without `proc.on('error', ...)` a spawn failure (e.g. ENOENT for a
missing binary) is emitted as an unlistened 'error' event, which
Node treats as an uncaught exception that bypasses the awaiting
try/catch and kills the process. The .deb hits this on first boot
because plugins.ts spawns `pnpm --version` for a startup log line
and pnpm isn't a runtime dep — Etherpad logs "Starting" then
immediately stops. Reject the promise on 'error' so the existing
try/catch in the caller actually catches it.
packaging/scripts/postinstall.sh:
chown /var/lib/etherpad/plugin_packages AFTER `cp -a` from the
staged tree — `cp -a` preserves source (root) ownership and was
re-rooting the directory we'd just chowned to etherpad. Same
ordering the var symlink block already used.
packaging/test-local.sh:
Run `CI=1 pnpm install --frozen-lockfile` before staging so the
package is built from a fresh, lockfile-consistent tree (matches
CI). Fixes spurious "Cannot find module 'X'" failures from stale
local symlinks pointing at out-of-date pnpm store paths.
End-to-end test now passes: postinstall asserts pass, /health
returns 200, dpkg --purge cleans up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop packaging/etc/settings.json.dist that snuck into the previous commit (generated at build time by test-local.sh / CI from settings.json.template). Add /staging/, /dist/, /packaging/etc/ to .gitignore so they don't recur. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit f2f0259 |
The startup IIFE that logs the pnpm version is informational only. pnpm is a dev-only dependency: admin-UI plugin install goes through live-plugin-manager directly, and plugin migration is short-circuited when var/installed_plugins.json is present (e.g. on packaged installs). A missing pnpm on PATH is therefore expected on hardened deployments and shouldn't surface as a red ERROR in journalctl. Detect ENOENT specifically and log at debug; treat other errors (permission denied, etc.) as warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| // Without this, a spawn failure (e.g. ENOENT for a missing binary) is | ||
| // emitted as an 'error' event with no listener, which Node.js treats as | ||
| // an uncaught exception that bypasses any try/catch around the awaited | ||
| // promise and kills the process. Reject the promise instead so callers | ||
| // can handle it. | ||
| proc.on('error', (err) => { | ||
| procFailedErr.message = `Failed to spawn ${args[0]}: ${(err as Error).message}`; | ||
| procFailedErr.code = (err as any).code; | ||
| logger.debug(procFailedErr.stack); | ||
| px.reject(procFailedErr); | ||
| }); |
There was a problem hiding this comment.
1. run_cmd spawn error untested 📘 Rule violation ☼ Reliability
The PR fixes run_cmd to reject on spawn errors (e.g. missing binary), but it does not add a regression test to ensure this behavior doesn’t silently regress. Without an automated test, the original process-killing failure mode could be reintroduced unnoticed.
Agent Prompt
## Issue description
A bug fix was added to `src/node/utils/run_cmd.ts` to handle `spawn` failures by rejecting the returned promise, but there is no regression test that would fail if this handler were removed.
## Issue Context
The new code handles the `ChildProcess` `error` event (e.g. `ENOENT`) and calls `px.reject(...)` instead of letting Node treat the unhandled `error` event as an uncaught exception that kills the process. Add a `vitest` test that calls `run_cmd()` with a definitely-missing binary name and asserts it rejects with an error that includes `code === 'ENOENT'` (or equivalent), ensuring the test suite would fail/crash if the handler were reverted.
## Fix Focus Areas
- src/node/utils/run_cmd.ts[149-159]
- src/tests/backend-new/specs/[add new test file exercising spawn ENOENT]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…rors CI gap: deb-package.yml only fired on v* tag pushes, so a PR that broke the .deb wasn't caught until release time. Wire it to PRs and develop pushes via a paths filter covering packaging files and the runtime files Etherpad needs at first boot. The release job already gates on `if: startsWith(github.ref, 'refs/tags/v')` so PR runs won't try to publish. Test gap: the run_cmd.ts spawn-error fix (commit 5eee789) had no test, which is how the bug shipped originally — plugins.ts spawned `pnpm --version` at startup, the rejection was never caught, and the .deb crashed mid-boot. Add a backend spec that exercises: - ENOENT for a missing binary -> rejects (regression test) - successful command -> resolves stdout - non-zero exit -> rejects with code backend-tests.yml's recursive mocha glob picks up the new spec automatically; no workflow change needed there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit b73c8cf |
| set -eux | ||
| sudo apt-get update | ||
| sudo apt-get install -y nodejs | ||
| sudo dpkg -i dist/*.deb || sudo apt-get install -f -y |
There was a problem hiding this comment.
1. Smoke test doesn't ensure node>=20 🐞 Bug ☼ Reliability
The deb-package workflow installs nodejs from the runner’s default APT sources without ensuring it meets the package dependency nodejs (>= 20), so dpkg -i can fail to configure (or behave differently) depending on the runner image. This makes the smoke-test nondeterministic and can invalidate the subsequent assertions/startup test.
Agent Prompt
### Issue description
The CI smoke test installs `nodejs` from default APT sources without ensuring it satisfies the package dependency (`nodejs (>= 20)`), making the smoke test nondeterministic and potentially invalid.
### Issue Context
- The `.deb` declares `Depends: nodejs (>= 20)`.
- The workflow currently does `apt-get install -y nodejs` with no repo setup/pinning.
- `packaging/test-local.sh` already uses NodeSource (`setup_lts.x`) before installing nodejs.
### Fix Focus Areas
- .github/workflows/deb-package.yml[136-139]
### Suggested fix
In the smoke-test step, install Node.js via a deterministic mechanism, for example:
- Add NodeSource LTS repo (`curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -`) then `sudo apt-get install -y nodejs`, **or**
- Use a pinned distro/repo that guarantees `nodejs >= 20`.
This should happen before `dpkg -i dist/*.deb` so the package can configure and `postinstall` assertions are meaningful.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…s node 18) ubuntu-latest's default apt nodejs is 18.19.1, but our package requires nodejs (>= 20). The smoke test was doing `apt-get install nodejs` followed by `dpkg -i ... || apt-get install -f`, which on a node-18 host fails the dep check, then `-f` "fixes" by REMOVING the etherpad package — and the next assertion (test -x /usr/bin/etherpad) crashes. Match what packaging/test-local.sh and the README recommend: install node from NodeSource (current LTS) before installing the .deb. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit 0c9c357 |
| chown -R etherpad:etherpad "${RUNTIME_VAR}" | ||
|
|
||
| # Plugin install paths. Etherpad's admin UI installs plugins into | ||
| # ${root}/src/plugin_packages and creates symlinks under | ||
| # ${root}/src/node_modules. Both are under /opt and would EACCES | ||
| # under the etherpad user without these adjustments. | ||
| PLUGIN_PKG_LIVE=/var/lib/etherpad/plugin_packages | ||
| PLUGIN_PKG_LINK="${APP_DIR}/src/plugin_packages" | ||
| NODE_MODULES_DIR="${APP_DIR}/src/node_modules" | ||
|
|
||
| mkdir -p "${PLUGIN_PKG_LIVE}" | ||
| if [ -e "${PLUGIN_PKG_LINK}" ] && [ ! -L "${PLUGIN_PKG_LINK}" ]; then | ||
| cp -a "${PLUGIN_PKG_LINK}/." "${PLUGIN_PKG_LIVE}/" 2>/dev/null || true | ||
| rm -rf "${PLUGIN_PKG_LINK}" | ||
| fi | ||
| # chown after the cp -- cp -a preserves the (root) ownership of the | ||
| # staged source files and would re-root anything we chowned earlier. | ||
| chown -R etherpad:etherpad "${PLUGIN_PKG_LIVE}" | ||
| ln -sfn "${PLUGIN_PKG_LIVE}" "${PLUGIN_PKG_LINK}" | ||
|
|
||
| # node_modules is bundled (root-owned contents); the directory itself | ||
| # must be group-writable by etherpad so plugin installs can create | ||
| # symlinks alongside the shipped packages. ReadWritePaths in the unit | ||
| # also exposes it as writable under ProtectSystem=strict. | ||
| if [ -d "${NODE_MODULES_DIR}" ]; then | ||
| chgrp etherpad "${NODE_MODULES_DIR}" | ||
| chmod 2775 "${NODE_MODULES_DIR}" | ||
| fi |
There was a problem hiding this comment.
1. Symlink chown escalation 🐞 Bug ⛨ Security
packaging/scripts/postinstall.sh recursively chown -R's /var/lib/etherpad/var and /var/lib/etherpad/plugin_packages after making them writable by the etherpad user, so a symlink planted inside these trees can redirect root’s chown onto arbitrary paths during an upgrade. This is a root privilege escalation vector triggered by dpkg running maintainer scripts as root.
Agent Prompt
### Issue description
`postinstall.sh` runs `chown -R` on directories that are writable by the `etherpad` service user. If the `etherpad` user (or an attacker who gained that user) plants symlinks inside those directories, a future package upgrade can cause the root-run postinstall to `chown` arbitrary files/directories (symlink attack).
### Issue Context
This runs under `dpkg` as root on upgrades (`postinst configure`). The writable directories are `/var/lib/etherpad/var` and `/var/lib/etherpad/plugin_packages`.
### Fix Focus Areas
- packaging/scripts/postinstall.sh[63-90]
### Suggested fix
- Replace recursive `chown -R` calls with a symlink-safe alternative, such as:
- `chown -hR etherpad:etherpad "$RUNTIME_VAR"` and `chown -hR etherpad:etherpad "$PLUGIN_PKG_LIVE"` (GNU coreutils), and/or
- `find -P "$dir" -xdev -exec chown etherpad:etherpad {} +` to ensure symlinks are not dereferenced.
- Keep ownership correction behavior for normal files/dirs, but ensure symlinks cannot cause traversal or ownership changes outside the intended trees.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
postinstall sets /etc/etherpad to 0750 root:etherpad (DB creds live here) and /var/lib/etherpad similarly. The GH Actions runner user isn't in the etherpad group, so 'test -f /etc/etherpad/settings.json' hits EACCES. Add sudo to each check that crosses one of those dirs. (Wrapping the whole block in `sudo bash <<EOF` would have been cleaner but YAML literal-block + heredoc terminator don't play well together at this indent.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit 3daf300 |
…adduser postinstall: Use `chown -hR` instead of `chown -R` on /var/lib/etherpad/var and /var/lib/etherpad/plugin_packages. Both directories are writable by the unprivileged etherpad service user, so a symlink planted there could redirect root's chown onto arbitrary system files (e.g. /etc/shadow) on the next `apt upgrade`. -hR makes chown act on the symlink itself rather than its target — standard mitigation for this TOCTOU-style local privilege escalation. nfpm: Move adduser from Depends to Pre-Depends. preinst creates the etherpad user before unpacking; with plain `dpkg -i` (no apt) the Depends list isn't installed beforehand, so a minimal system without adduser would fail preinst before unpack and apt-get -f couldn't recover. Pre-Depends guarantees adduser is configured first. Both flagged in Qodo's persistent review of 3daf300. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit 7daaf07 |
nfpm's Overridables schema doesn't include predepends; it's a deb-only top-level field. Previous commit nested it under overrides.deb, which caused nfpm to reject the entire manifest with "field predepends not found in type nfpm.Overridables" and broke both arch builds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
…l, disable on remove, writable settings)
deb-package.yml:
- Move 'Resolve version' (which calls `node -p`) to AFTER setup-node
so it doesn't depend on the runner image preinstalling node.
- Replace `curl ... | sudo bash` NodeSource installer with the
explicit gpg-key + sources.list approach. Same outcome (NodeSource
LTS apt repo), but no execution of network-fetched code as root.
Reduces blast radius if NodeSource's setup endpoint is ever
compromised — we only trust the signed apt repo metadata.
postinstall.sh:
- /etc/etherpad/settings.json now etherpad:etherpad mode 0660 (was
root:etherpad 0640). The admin /admin/settings UI persists changes
by writing back to settings.settingsFilename; with the previous
perms the etherpad user could read but not write, so saving via
the admin UI failed silently. Group-only access preserved (DB
creds still unreadable by other users).
postremove.sh:
- On `dpkg --remove`, run `systemctl disable etherpad.service` before
`daemon-reload` so the wants/ symlink doesn't dangle after dpkg
deletes the unit file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit 1173fcb |
…ERSION deb-package.yml: Workflow-level permissions was `contents: write` so the build job got write access on every PR run, even though only the release job needs it (to attach release assets). Narrow the workflow default to `contents: read` and let the release job opt back in to write — it already declares its own job-level `contents: write` block, so this is just removing an over-broad default. test-local.sh: The script defined NFPM_VERSION but then unconditionally ran `goreleaser/nfpm:latest`, so local builds could diverge from CI's pinned v2.43.0. Use the variable in the docker tag (stripping the leading "v" to match the image's tag scheme). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/review |
|
Persistent review updated to latest commit 287497b |
| # Etherpad reads settings.json from CWD (/opt/etherpad). Expose | ||
| # the /etc copy there via symlink. | ||
| ln -sfn "${ACTIVE_SETTINGS}" "${APP_DIR}/settings.json" |
There was a problem hiding this comment.
1. Settings.json clobbered 🐞 Bug ☼ Reliability
packaging/scripts/postinstall.sh unconditionally force-replaces /opt/etherpad/settings.json with a symlink to /etc/etherpad/settings.json, deleting any pre-existing non-symlink settings.json. This can silently wipe an existing installation’s configuration (DB/auth/etc) when transitioning to the .deb, potentially preventing correct startup.
Agent Prompt
### Issue description
The postinstall script forcefully replaces `/opt/etherpad/settings.json` with a symlink to `/etc/etherpad/settings.json`. If a user already has a real (non-symlink) `settings.json` at `/opt/etherpad/settings.json` (common for manual installs / legacy packaging), it will be deleted, losing configuration.
### Issue Context
Etherpad defaults to loading `settings.json` from the Etherpad root directory. The repo docs and legacy init config reference editing `/opt/etherpad/settings.json`, so it’s realistic that this file exists before installing the new .deb.
### Fix Focus Areas
- packaging/scripts/postinstall.sh[23-43]
### Suggested fix
Before `ln -sfn ... /opt/etherpad/settings.json`, add migration/backup logic:
- If `${APP_DIR}/settings.json` exists and is **not** a symlink:
- If `${ACTIVE_SETTINGS}` does **not** exist yet, copy/move the existing `${APP_DIR}/settings.json` to `${ACTIVE_SETTINGS}` (preserving ownership/permissions appropriately), then proceed to symlink.
- Else (both exist), keep `/etc` as source of truth but **back up** the old `${APP_DIR}/settings.json` to something like `${APP_DIR}/settings.json.bak.<timestamp>` and log a message to stdout/stderr.
- Only then run `ln -sfn`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
Reintroduces the Debian packaging from #7559 (reverted in #7582) with fixes for review feedback from #7559 and #7583:
etherpad-lite→etherpadeverywhere — package name, installed paths (/opt/etherpad,/etc/etherpad,/var/lib/etherpad,/var/log/etherpad), systemd unit (etherpad.service), CLI wrapper (/usr/bin/etherpad),Documentation=URL, and the.debfilename. Keptreplaces/conflicts/provides: etherpad-liteso any dev builds upgrade cleanly.sqlite(wasdirty).postinstallrewrites the seeded/etc/etherpad/settings.jsontodbType: "sqlite"pointed at/var/lib/etherpad/etherpad.db./opt/etherpad/var→/var/lib/etherpad/varsymlink inpostinstall. Etherpad writes runtime files (var/js,var/installed_plugins.json, …) undersettings.root/var, which resolves to/opt/etherpad/varin this layout —ProtectSystem=strictblocks that. The symlink keeps the existingReadWritePaths=/var/lib/etherpadsufficient and avoids opening/optfor writes. (Fixes Qodo bug "Readonly /opt breaks startup".)installed_plugins.jsoninpostinstallwith{"plugins":[{"name":"ep_etherpad-lite","version":"<v>"}]}. OtherwisecheckForMigration()falls back topnpm lson first boot, butpnpmis not a runtime dep — and the bundlednode_modulesalready contains every shipped plugin, so no migration is needed. Also prevents accidental network plugin installs at first run. (Fixes Qodo bug "Plugin migration startup failure".).debsha256 against upstreamchecksums.txtbeforesudo dpkg -iin CI. (Fixes Qodo bug "Unverified nfpm download".)Depends: nodejs (>= 20)floor is unchanged — that mirrors Etherpad'sengines.node.etherpad-latest_<arch>.debaliases attached to each GitHub Release, so users cancurl …/releases/latest/download/etherpad-latest_amd64.debwithout knowing the version. README updated.2.6.1example, document the latest URL, point tosetup_lts.xinstead of pinning a NodeSource major.What you get installing the package
/opt/etherpadwith prebuilt, self-containednode_modules/— no pnpm at runtime, onlynodejs (>= 20).etherpadsystem user/group, created viaadduserinpreinst./etc/etherpad/settings.json(seeded sqlite default; preserved across upgrades; removed onpurge)./var/lib/etherpadwritable underProtectSystem=strict, withvar/symlinked from/opt/etherpad/varso first-boot writes succeed./lib/systemd/system/etherpad.service— hardened unit (NoNewPrivileges,ProtectSystem=strict,ProtectHome,PrivateTmp,RestrictAddressFamilies) withRestart=on-failure./usr/bin/etherpadCLI wrapper runningnode --import tsx/esm.CI smoke test
.github/workflows/deb-package.ymltriggers onv*tags, builds amd64 + arm64 via native runners (ubuntu-latest+ubuntu-24.04-arm), and on amd64:dpkg -iinstalls the Etherpad.deb/opt/etherpad/varsymlinks to/var/lib/etherpad/varinstalled_plugins.jsonexists and listsep_etherpad-lite/etc/etherpad/settings.jsoncontains"dbType": "sqlite"systemctl start etherpadcurl /healthreturns 200 (exits non-zero + dumpsjournalctlif not)dpkg --purgeremoves config + userThe
releasejob attaches bothetherpad_<ver>_<arch>.debandetherpad-latest_<arch>.debto the GitHub Release.Not included (follow-up)
Publishing to an APT repo (Cloudsmith / Launchpad PPA / self-hosted reprepro) is out of scope — needs a governance decision on who holds the signing key. Recipes are in
packaging/README.md.Legacy
bin/buildDebian.shandbin/deb-src/are stale (Etherpad v1.3, init-based). Flagged for removal in a follow-up PR.Test plan
pnpm install --frozen-lockfile && pnpm run build:etherpadsucceedsnfpm package --packager debproduces a well-formed.deb(dpkg-deb -I/-c)/healthreturns OK,/etc/etherpad/settings.jsonshows"dbType": "sqlite",/opt/etherpad/var → /var/lib/etherpad/var,installed_plugins.jsonis seeded,/var/lib/etherpad/etherpad.dbis created by the etherpad userpnpm(checkjournalctl -u etherpadfor any pnpm references)/etc/etherpad/settings.jsonandinstalled_plugins.jsonuntouched; service restartedapt removekeeps/etcand/var/lib;apt purgeremoves them plus theetherpaduseretherpad-latest_<arch>.debaliases attached to releaseRefs #7529, #7559, #7582
🤖 Generated with Claude Code